2.4 도메인 영역의 주요 구성 요소
- 도메인: 고유의 식별자를 갖는 객체로 자신의 라이프 사이클을 갖는다. 도메인 모델의 데이터를 포함하며 해당 데이터와 관련된 기능을 함께 제공한다.
- 밸류: 고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 값을 표현할 때 사용된다. 엔티티의 속성으로 사용할 뿐만 아니라 다른 밸류 타입의 속성으로도 사용할 수 있다.
- 애그리거트: 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것
- 리포지터리: 도메인 모델의 영속성을 처리한다.
- 도메인 서비스: 특정 엔티티에 속하지 않은 도메인 로직을 제공한다. 도메인 로직이 여러 엔티티와 밸류를 필요로 하면 도메인 서비스에서 로직을 구현한다.
엔티티와 밸류
도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다.
도메인 관점에서 기능을 구현하고 기능 구현을 캡슐화해서 데이터가 임의로 변경되는 것을 막는다.
애그리거트
엔티티와 밸류 개수가 많아질수록 모델은 점점 더 복잡해진다.
도메인 모델에서 전체 구조를 이해하는 데 도움이 되는 것이 애그리거트다.
애그리거트를 사용하면 개별 객체가 아닌 관련 객체를 묶어서 객체 군집 단위로 모델을 바라볼 수 있게 된다. 이를 통해 큰 틀에서 도메인 모델을 관리할 수 있다.
애그리거트는 군집에 속한 객체를 관리하는 루트 엔티티를 갖는다. 루트 엔티티는 애그리거트에 속해 있는 엔티티와 밸류 객체를 이용해서 애그리거트가 구현해야 할 기능을 제공한다.
애그리거트를 사용하는 코드는 애그리거트 루트를 통해 간접적으로 애그리거트 내의 다른 엔티티나 밸류 객체에 접근한다.
리포지터리
도메인 객체를 지속적으로 사용하기 위해 물리적인 저장소에 도메인 객체를 보관해야 하는데, 이런 구현을 위한 도메인 모델이다.
리포지터리는 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다.
리포지터리를 사용하는 주체가 응용 서비스이기 때문에 리포지터리는 응용 서비스가 필요로 하는 기능을 제공한다.
- 애그리거트를 저장하는 메서드
- 애그리거트 루트 식별자로 애그리거트를 조회하는 메서드
3.1 애그리거트
애그리거트는 일관성을 관리하는 기준도 된다. 복잡한 도메인을 단순한 구조로 만들 수 있다.
한 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다. 도메인 규칙에 따라 최처 시점에 일부 객체를 만들 필요가 없는 경우도 있지만 애그리거트에 속한 구성요소는 대부분 함께 생성하고 함께 제거한다.
애그리거트는 경계를 갖는다. 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다. 각 애그리거트는 자기 자신을 관리할 뿐 다른 애그리거트를 관리하지 않는다.
경계를 설정할 때 기본이 되는 것은 도메인 규칙과 요구사항이다.
도메인 규칙에 따라 함께 생성되는 구성요소는 한 애그리거트에 속할 가능성이 높다. 또한 함께 변경되는 빈도가 높은 객체는 한 애그리거트에 속할 가능성이 높다.
‘A가 B를 갖는다’로 해석할 수 있는 요구사항이 있다고 하더라도 이것은 반드시 A와 B가 한 애그리거트에 속한다는 것을 의미하는 것은 아니다. 좋은 예시에는 상품과 리뷰가 있다.
애그리거트 루트
애그리거트는 여러 객체로 구성되기 때문에 한 객체만 상태가 정상이면 안 된다. 도메인 규칙을 지키려면 애그리거트에 속한 모든 객체가 정상 상태를 가져야 한다.
애그리거트에 속한 모든 객체가 일관된 상태를 유지하려면 애그리거트 전체를 관리할 주체가 필요한데, 이 책임을 지는 것이 바로 애그리거트의 루트 엔티티이다. 애그리거트에 속한 객체는 애그리거트 루트 엔티티에 직접 또는 간접적으로 속하게 된다.
도메인 규칙과 일관성
애그리거트 루트의 핵심 역할은 애그리거트의 일관성이 깨지지 않게 하는 것이다. 이를 위해 애그리거트 루트는 애그리거트가 제공해야 할 도메인 기능을 구현한다.
애그리거트 외부에서 애그리거트에 속한 객체를 직접 변경하면 안 된다.
불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들려면 도메인 모델에 대해 다음의 두 가지를 습관적으로 적용해야 한다.
- 단순히 필드를 변경하는 set 메서드를 public 으로 만들지 않는다.
- 밸류 타입은 불변으로 구현한다.
애그리거트 루트의 기능 구현
애그리거트 루트는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성한다.
애그리거트 루트는 기능 실행을 위임하기도 한다.
트랜잭션 범위
트랜잭션 범위는 작을수록 좋다.
한 트랜잭션에서는 한 개의 애그리거트만 수정해야 한다. 한 트랜잭션에서 두 개 이상의 애그리거트를 수정하면 트랜잭션 충돌이 발생할 가능성이 더 높아지기 때문에 한 번에 수정하는 애그리거트 개수가 많아질수록 전체 처리량이 떨어진다.
한 트랜잭션에서 한 애그리거트만 수정한다는 것은 애그리거트에서 다른 애그리거트를 변경하지 않는다는 것을 의미한다.
부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 한다면 애그리거트에서 다른 애그리거트를 직접 수정하지 말고 응용 서비스에서 두 애그리거트를 수정하도록 구현한다.
3.3 리포지터리와 애그리거트
애그리거트는 개념적으로 하나이므로 리포지터리는 애그리거트 전체를 저장소에 영속화해야 한다.
동일하게 애그리거트를 구하는 리포지터리 메서드는 완전한 애그리거트를 제공해야 한다.
애그리거트의 상태가 변경되면 모든 변경을 원자적으로 저장소에 반영해야 한다.
3.4 ID를 이용한 애그리거트 참조
애그리거트를 직접 참조할 때 발생할 수 있는 문제점은 편리함을 오용할 수 있다는 것이다. 한 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면 다른 애그리거트의 상태를 쉽게 변경할 수 있게 된다.
애그리거트를 직접 참조하면 성능과 관련된 여러 가지 고민을 해야 한다.
또한 확장에서도 불리하다.
이런 문제를 완화하기 위해 ID를 이용해서 다른 애그리거트를 참조하는 것이다.
ID 참조를 사용하면 모든 객체가 참조로 연결되지 않고 한 애그리거트에 속한 객체들만 참조로 연결된다. 이는 애그리거트의 경계를 명화갛게 하고 애그리거트간 물리적인 연결을 제거하기 때문에 모델의 복잡도를 낮춰준다. 또한 애그리거트 간의 의존을 제거하여 응집도를 높여준다.
ID를 이용한 참조화 조회 성능
ID 참조 방식을 사용하면서 N + 1 조회와 같은 문제가 발생하지 않도록 하려면 조회 전용 쿼리를 사용하면 된다.